﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using System.ComponentModel;
using System.Reflection;
using System.Collections;

namespace Kokomo.Xml
{
	public class XmlObjectSerializer
	{
		public XmlObjectSerializer(NamespaceSchema defaultNamespace)
		{
			ArgumentValidation.CheckNullArgument("defaultNamespace", defaultNamespace);

			this.DefaultNamespace = defaultNamespace;
			this.Namespaces = new NamespaceSchemaCollection();
			this.TypeConverterTable = new CachedLookupTable<Type, TypeConverter>(CreateTypeConverter);
		}

		public void Serialize(object obj, Stream stream)
		{
			ArgumentValidation.CheckNullArgument("stream", stream);

			using (StreamWriter writer = new StreamWriter(stream))
			{
				this.Serialize(obj, writer);
			}
		}

		public NamespaceSchema DefaultNamespace { get; private set; }
		public NamespaceSchemaCollection Namespaces { get; private set; }

		public CachedLookupTable<Type, TypeConverter> TypeConverterTable { get; private set; }

		protected static TypeConverter CreateTypeConverter(Type type)
		{
			ArgumentValidation.CheckNullArgument("type", type);

			// Check for explicit TypeConverter
			TypeConverterAttribute attribute = type.GetCustomAttribute<TypeConverterAttribute>();
			TypeConverter converter = null;
			if (attribute != null)
			{
				Type converterType = Type.GetType(attribute.ConverterTypeName);
				converter = (TypeConverter)Activator.CreateInstance(converterType);
			}
			if (converter == null)
			{
				// Check for Parse method
				MethodInfo parseMethod = type.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static);
				if (parseMethod != null)
				{
					Type converterDelegateType = typeof(Func<,>).MakeGenericType(typeof(string), type);
					Delegate converterDelegate = Delegate.CreateDelegate(converterDelegateType, parseMethod);

					MethodInfo toStringMethod = type.GetMethod("ToString");
					Type inverseConverterDelegateType = typeof(Func<,>).MakeGenericType(type, typeof(string));
					Delegate inverseConverterDelegate = Delegate.CreateDelegate(inverseConverterDelegateType, toStringMethod);

					Type converterType = typeof(MethodBasedTypeConverter<,>).MakeGenericType(typeof(string), type);
					converter = (TypeConverter)Activator.CreateInstance(converterType, converterDelegate, inverseConverterDelegateType);
				}
			}

			return converter;
		}

		public void Serialize(object obj, TextWriter writer)
		{
			ArgumentValidation.CheckNullArgument("writer", writer);

			using (XmlTextWriter xmlWriter = new XmlTextWriter(writer))
			{
				xmlWriter.WriteStartDocument();
				this.Serialize(obj, xmlWriter);
			}
		}
		public void Serialize(object obj, XmlWriter writer)
		{
			ArgumentValidation.CheckNullArgument("obj", obj);
			ArgumentValidation.CheckNullArgument("writer", writer);

			SerializeObject(obj, writer);
		}

		protected void SerializeObject(object obj, XmlWriter writer)
		{
			string elementNamespace;
			TypeSchema typeSchema = this.GetTypeSchema(obj.GetType(), out elementNamespace);
			writer.WriteStartElement(elementNamespace, typeSchema.ElementName, null);

			// Serialize attributes
			foreach (var propertySchema in typeSchema.Properties.ByNodeType(PropertyNodeType.Attribute))
			{
				object value = propertySchema.GetValue(obj);
				string text;
				if (propertySchema.PropertyType == typeof(string))
				{
					text = (string)value;
				}
				else
				{
					TypeConverter converter = this.TypeConverterTable.GetItem(propertySchema.PropertyType);
					text = (string)converter.ConvertFrom(value);
				}

				if (text != null)
				{
					writer.WriteAttributeString(propertySchema.NodeName, text);
				}
			}

			// Serialize elements
			foreach (var propertySchema in typeSchema.Properties.ByNodeType(PropertyNodeType.Element))
			{
				object value = propertySchema.GetValue(obj);
				if (value != null)
				{
					writer.WriteStartElement(propertySchema.NodeName);

					IEnumerable enumerable = value as IEnumerable;
					if (enumerable != null && (propertySchema.PropertyType.IsArray || propertySchema.UseImplicitList))
					{
						this.SerializeList(enumerable, writer);
					}
					else if (value != null)
					{
						this.SerializeObject(value, writer);
					}
					writer.WriteEndElement();
				}
			}
		}

		protected void SerializeList(IEnumerable enumerable, XmlWriter writer)
		{
			foreach (var item in enumerable)
			{
				this.SerializeObject(enumerable, writer);
			}
		}

		#region Namespaces

		protected TypeSchema GetTypeSchema(Type type, out string prefix)
		{
			ArgumentValidation.CheckNullArgument("type", type);

			prefix = null;
			TypeSchema typeSchema = this.DefaultNamespace.GetTypeSchema(type);
			if (typeSchema == null)
			{
				foreach (var schema in this.Namespaces)
				{
					typeSchema = schema.GetTypeSchema(type);
					if (typeSchema != null)
					{
						prefix = schema.Name;
						break;
					}
				}
			}

			return typeSchema;
		}

		#endregion
	}
}
